InputControlVisualizer.cs (13275B)
1 using System; 2 using System.Collections.Generic; 3 using UnityEngine.InputSystem.Layouts; 4 using UnityEngine.InputSystem.LowLevel; 5 6 ////TODO: add way to plot values over time 7 8 // Goal is to build this out into something that can visualize a large number of 9 // aspects about an InputControl/InputDevice especially with an eye towards making 10 // it a good deal to debug any input collection/processing irregularities that may 11 // be seen in players (or the editor, for that matter). 12 13 // Some fields assigned through only through serialization. 14 #pragma warning disable CS0649 15 16 namespace UnityEngine.InputSystem.Samples 17 { 18 /// <summary> 19 /// A component for debugging purposes that adds an on-screen display which shows 20 /// activity on an input control over time. 21 /// </summary> 22 /// <remarks> 23 /// This component is most useful for debugging input directly on the source device. 24 /// </remarks> 25 /// <seealso cref="InputActionVisualizer"/> 26 [AddComponentMenu("Input/Debug/Input Control Visualizer")] 27 [ExecuteInEditMode] 28 public class InputControlVisualizer : InputVisualizer 29 { 30 /// <summary> 31 /// What kind of visualization to show. 32 /// </summary> 33 public Mode visualization 34 { 35 get => m_Visualization; 36 set 37 { 38 if (m_Visualization == value) 39 return; 40 m_Visualization = value; 41 SetupVisualizer(); 42 } 43 } 44 45 /// <summary> 46 /// Path of the control that is to be visualized. 47 /// </summary> 48 /// <seealso cref="InputControlPath"/> 49 /// <seealso cref="InputControl.path"/> 50 public string controlPath 51 { 52 get => m_ControlPath; 53 set 54 { 55 m_ControlPath = value; 56 if (m_Control != null) 57 ResolveControl(); 58 } 59 } 60 61 /// <summary> 62 /// If, at runtime, multiple controls are matching <see cref="controlPath"/>, this property 63 /// determines the index of the control that is retrieved from the possible options. 64 /// </summary> 65 public int controlIndex 66 { 67 get => m_ControlIndex; 68 set 69 { 70 m_ControlIndex = value; 71 if (m_Control != null) 72 ResolveControl(); 73 } 74 } 75 76 /// <summary> 77 /// The control resolved from <see cref="controlPath"/> at runtime. May be null. 78 /// </summary> 79 public InputControl control => m_Control; 80 81 protected new void OnEnable() 82 { 83 if (m_Visualization == Mode.None) 84 return; 85 86 if (s_EnabledInstances == null) 87 s_EnabledInstances = new List<InputControlVisualizer>(); 88 if (s_EnabledInstances.Count == 0) 89 { 90 InputSystem.onDeviceChange += OnDeviceChange; 91 InputSystem.onEvent += OnEvent; 92 } 93 s_EnabledInstances.Add(this); 94 95 ResolveControl(); 96 97 base.OnEnable(); 98 } 99 100 protected new void OnDisable() 101 { 102 if (m_Visualization == Mode.None) 103 return; 104 105 s_EnabledInstances.Remove(this); 106 if (s_EnabledInstances.Count == 0) 107 { 108 InputSystem.onDeviceChange -= OnDeviceChange; 109 InputSystem.onEvent -= OnEvent; 110 } 111 112 m_Control = null; 113 114 base.OnDisable(); 115 } 116 117 protected new void OnGUI() 118 { 119 if (m_Visualization == Mode.None) 120 return; 121 122 base.OnGUI(); 123 } 124 125 protected new void OnValidate() 126 { 127 ResolveControl(); 128 base.OnValidate(); 129 } 130 131 [Tooltip("The type of visualization to perform for the control.")] 132 [SerializeField] private Mode m_Visualization; 133 [Tooltip("Path of the control that should be visualized. If at runtime, multiple " 134 + "controls match the given path, the 'Control Index' property can be used to decide " 135 + "which of the controls to visualize.")] 136 [InputControl, SerializeField] private string m_ControlPath; 137 [Tooltip("If multiple controls match 'Control Path' at runtime, this property decides " 138 + "which control to visualize from the list of candidates. It is a zero-based index.")] 139 [SerializeField] private int m_ControlIndex; 140 141 [NonSerialized] private InputControl m_Control; 142 143 private static List<InputControlVisualizer> s_EnabledInstances; 144 145 private void ResolveControl() 146 { 147 m_Control = null; 148 if (string.IsNullOrEmpty(m_ControlPath)) 149 return; 150 151 using (var candidates = InputSystem.FindControls(m_ControlPath)) 152 { 153 var numCandidates = candidates.Count; 154 if (numCandidates > 1 && m_ControlIndex < numCandidates && m_ControlIndex >= 0) 155 m_Control = candidates[m_ControlIndex]; 156 else if (numCandidates > 0) 157 m_Control = candidates[0]; 158 } 159 160 SetupVisualizer(); 161 } 162 163 private void SetupVisualizer() 164 { 165 if (m_Control == null) 166 { 167 m_Visualizer = null; 168 return; 169 } 170 171 switch (m_Visualization) 172 { 173 case Mode.Value: 174 { 175 var valueType = m_Control.valueType; 176 if (valueType == typeof(Vector2)) 177 m_Visualizer = new VisualizationHelpers.Vector2Visualizer(m_HistorySamples); 178 else if (valueType == typeof(float)) 179 m_Visualizer = new VisualizationHelpers.ScalarVisualizer<float>(m_HistorySamples) 180 { 181 ////TODO: pass actual min/max limits of control 182 limitMax = 1, 183 limitMin = 0 184 }; 185 else if (valueType == typeof(int)) 186 m_Visualizer = new VisualizationHelpers.ScalarVisualizer<int>(m_HistorySamples) 187 { 188 ////TODO: pass actual min/max limits of control 189 limitMax = 1, 190 limitMin = 0 191 }; 192 else 193 { 194 ////TODO: generic visualizer 195 } 196 break; 197 } 198 199 case Mode.Events: 200 { 201 var visualizer = new VisualizationHelpers.TimelineVisualizer(m_HistorySamples) 202 { 203 timeUnit = VisualizationHelpers.TimelineVisualizer.TimeUnit.Frames, 204 historyDepth = m_HistorySamples, 205 showLimits = true, 206 limitsY = new Vector2(0, 5) // Will expand upward automatically 207 }; 208 m_Visualizer = visualizer; 209 visualizer.AddTimeline("Events", Color.green, 210 VisualizationHelpers.TimelineVisualizer.PlotType.BarChart); 211 break; 212 } 213 214 case Mode.MaximumLag: 215 { 216 var visualizer = new VisualizationHelpers.TimelineVisualizer(m_HistorySamples) 217 { 218 timeUnit = VisualizationHelpers.TimelineVisualizer.TimeUnit.Frames, 219 historyDepth = m_HistorySamples, 220 valueUnit = new GUIContent("ms"), 221 showLimits = true, 222 limitsY = new Vector2(0, 6) 223 }; 224 m_Visualizer = visualizer; 225 visualizer.AddTimeline("MaxLag", Color.red, 226 VisualizationHelpers.TimelineVisualizer.PlotType.BarChart); 227 break; 228 } 229 230 case Mode.Bytes: 231 { 232 var visualizer = new VisualizationHelpers.TimelineVisualizer(m_HistorySamples) 233 { 234 timeUnit = VisualizationHelpers.TimelineVisualizer.TimeUnit.Frames, 235 valueUnit = new GUIContent("bytes"), 236 historyDepth = m_HistorySamples, 237 showLimits = true, 238 limitsY = new Vector2(0, 64) 239 }; 240 m_Visualizer = visualizer; 241 visualizer.AddTimeline("Bytes", Color.red, 242 VisualizationHelpers.TimelineVisualizer.PlotType.BarChart); 243 break; 244 } 245 246 default: 247 throw new NotImplementedException(); 248 } 249 } 250 251 private static void OnDeviceChange(InputDevice device, InputDeviceChange change) 252 { 253 if (change != InputDeviceChange.Added && change != InputDeviceChange.Removed) 254 return; 255 256 for (var i = 0; i < s_EnabledInstances.Count; ++i) 257 { 258 var component = s_EnabledInstances[i]; 259 if (change == InputDeviceChange.Removed && component.m_Control != null && 260 component.m_Control.device == device) 261 component.ResolveControl(); 262 else if (change == InputDeviceChange.Added) 263 component.ResolveControl(); 264 } 265 } 266 267 private static void OnEvent(InputEventPtr eventPtr, InputDevice device) 268 { 269 // Ignore very first update as we usually get huge lag spikes and event count 270 // spikes in it from stuff that has accumulated while going into play mode or 271 // starting up the player. 272 if (InputState.updateCount <= 1) 273 return; 274 275 if (InputState.currentUpdateType == InputUpdateType.Editor) 276 return; 277 278 if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) 279 return; 280 281 for (var i = 0; i < s_EnabledInstances.Count; ++i) 282 { 283 var component = s_EnabledInstances[i]; 284 if (component.m_Control?.device != device || component.m_Visualizer == null) 285 continue; 286 287 component.OnEventImpl(eventPtr); 288 } 289 } 290 291 private unsafe void OnEventImpl(InputEventPtr eventPtr) 292 { 293 switch (m_Visualization) 294 { 295 case Mode.Value: 296 { 297 var statePtr = m_Control.GetStatePtrFromStateEvent(eventPtr); 298 if (statePtr == null) 299 return; // No value for control in event. 300 var value = m_Control.ReadValueFromStateAsObject(statePtr); 301 m_Visualizer.AddSample(value, eventPtr.time); 302 break; 303 } 304 305 case Mode.Events: 306 { 307 var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer; 308 var frame = (int)InputState.updateCount; 309 ref var valueRef = ref visualizer.GetOrCreateSample(0, frame); 310 var value = valueRef.ToInt32() + 1; 311 valueRef = value; 312 visualizer.limitsY = 313 new Vector2(0, Mathf.Max(value, visualizer.limitsY.y)); 314 break; 315 } 316 317 case Mode.MaximumLag: 318 { 319 var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer; 320 var lag = (Time.realtimeSinceStartup - eventPtr.time) * 1000; // In milliseconds. 321 var frame = (int)InputState.updateCount; 322 ref var valueRef = ref visualizer.GetOrCreateSample(0, frame); 323 324 if (lag > valueRef.ToDouble()) 325 { 326 valueRef = lag; 327 if (lag > visualizer.limitsY.y) 328 visualizer.limitsY = new Vector2(0, Mathf.Ceil((float)lag)); 329 } 330 break; 331 } 332 333 case Mode.Bytes: 334 { 335 var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer; 336 var frame = (int)InputState.updateCount; 337 ref var valueRef = ref visualizer.GetOrCreateSample(0, frame); 338 var value = valueRef.ToInt32() + eventPtr.sizeInBytes; 339 valueRef = value; 340 visualizer.limitsY = 341 new Vector2(0, Mathf.Max(value, visualizer.limitsY.y)); 342 break; 343 } 344 } 345 } 346 347 /// <summary> 348 /// Determines which aspect of the control should be visualized. 349 /// </summary> 350 public enum Mode 351 { 352 None = 0, 353 Value = 1, 354 Events = 4, 355 MaximumLag = 6, 356 Bytes = 7, 357 } 358 } 359 }